跳到主要内容

Roles 与项目组织

上一篇的 playbook 写长了靠 import_tasks 拆文件续命,但拆出来的文件还是死的:换个项目想用,得连文件带变量一起拷过去再手工改。Role 是 Ansible 官方的复用单元,把「干一件事需要的任务、模板、默认值」按固定目录结构打包,playbook 里一行引用就能用。这篇讲 role 的结构、变量分工、从 Galaxy 拿现成 role,以及一个项目的推荐目录布局。

为什么要用 Role​

单个 playbook 超过两三百行后,问题会集中爆发:

  • 任务、模板、变量、handler 全堆在一个文件里,改一处要翻半天
  • 想给新项目复用「装 nginx」这段逻辑,只能复制粘贴,改 bug 要同步 N 份
  • 默认值和项目特定值混在一起,换个环境不知道哪些该改

Role 的解法是靠约定好的目录结构:每类东西放固定位置,Ansible 自动去找。调用方只需要 roles: [nginx] 一行,剩下的按约定走。

Role 目录骨架​

一个名为 nginx 的 role 完整结构如下:

roles/nginx/
├── tasks/
│ └── main.yml # 任务入口,必须有
├── handlers/
│ └── main.yml # handler 定义
├── templates/
│ └── nginx.conf.j2 # Jinja2 模板,template 模块从这里取
├── files/
│ └── index.html # 静态文件,copy 模块从这里取
├── vars/
│ └── main.yml # role 内部固定变量,优先级高
├── defaults/
│ └── main.yml # 默认变量,优先级最低,给调用方覆盖用
└── meta/
└── main.yml # galaxy 元信息与依赖声明

各目录职责:

目录放什么说明
tasks任务列表role 的执行主体,main.yml 是入口,内部可再 import_tasks 拆分
handlershandlernotify 时按名字自动找到,不用写路径
templates.j2 模板template 模块的 src 直接写文件名,自动来这找
files静态文件copy 模块的 src 直接写文件名,自动来这找
defaults默认变量优先级全 Ansible 最低,专门留给调用方覆盖
vars内部变量优先级很高,几乎不会被覆盖
meta元信息galaxy 信息、role 依赖
目录可以省略

用不到的目录不用建。一个只装包的 role 只有 tasks/main.yml 加 defaults/main.yml 也完全合法。每个目录下的入口文件固定叫 main.yml。

defaults 与 vars 的分工​

两个目录都放变量,区别只有一个:优先级。

  • defaults/main.yml:优先级全 Ansible 最低,任何地方的同名定义都能覆盖它。放「用户可以改的默认值」
  • vars/main.yml:优先级很高,只有 -e、include_vars 等少数手段能盖过它。放「role 内部写死的常量」

用 nginx role 举例:

# roles/nginx/defaults/main.yml —— 用户可能想改的都放这
nginx_port: 80
nginx_worker_processes: auto
nginx_site_root: /var/www/html
# roles/nginx/vars/main.yml —— role 内部实现细节,不欢迎外部改
nginx_service_name: nginx
nginx_config_path: /etc/nginx/nginx.conf

调用方想换端口,在自己的 group_vars 或 playbook vars 里定义 nginx_port: 8080 就生效了;而 nginx_service_name 这种跟系统包名绑死的值,放 vars 里避免被手滑覆盖。

判断标准

写变量时问自己一句:「别的项目复用这个 role 时,这个值可能不一样吗?」可能 → defaults,不可能 → vars。

meta/main.yml​

meta/main.yml 有两块内容:galaxy 元信息(发布到 Galaxy 时用)和 role 依赖声明。

---
galaxy_info:
author: kuizuo
description: 在 Ubuntu 上安装并配置 nginx
license: MIT
min_ansible_version: "2.16"
platforms:
- name: Ubuntu
versions:
- jammy # 22.04

dependencies:
- role: common # 执行本 role 之前先跑 common
vars:
common_upgrade_all: false

dependencies 里声明的 role 会在本 role 之前自动执行,可以传变量。常用于「所有 role 都依赖 common(装基础工具、配时区)」这种场景,避免每个 role 里重复写。

依赖只执行一次

多个 role 依赖同一个 role 时,默认它只会被执行一次(参数完全相同的情况下)。需要重复执行时在该 role 的 meta 里设 allow_duplicates: true,或者改用 include_role 在 tasks 里显式调用。

使用 Role​

playbook 里引用 role 的写法,比 tasks 平铺清爽得多:

---
- name: 部署 web 站点
hosts: web
become: true

roles:
- common
- role: nginx
vars:
nginx_port: 8080

roles: 列表里写 role 名,可以顺带给这个 role 传变量。role 在 play 的 tasks 之前执行(想要更细的执行顺序控制,用 pre_tasks / tasks / post_tasks 三段,或在 tasks 里用 ansible.builtin.include_role)。

Ansible 按以下顺序找 role:

  1. playbook 所在目录下的 roles/ 子目录(最常用)
  2. playbook 同级目录本身
  3. ansible.cfg 里 roles_path 指定的路径
  4. 默认的 ~/.ansible/roles、/usr/share/ansible/roles、/etc/ansible/roles

手工建骨架要建一堆目录,用 ansible-galaxy init 一键生成:

cd roles/
ansible-galaxy init nginx

生成的骨架里每个目录都带一个 main.yml 空文件,把用不到的目录删掉即可。

Galaxy 与外部 role​

Ansible Galaxy 是官方的 role 共享站,很多基础活儿(装 Docker、配 MySQL)有现成 role,不用自己造轮子。

装单个 role:

ansible-galaxy role install geerlingguy.docker

role 多的时候写 requirements.yml 批量管理,团队共用一份:

---
roles:
- name: geerlingguy.docker
version: "7.4.1" # 锁定版本

- name: nginxinc.nginx
version: "0.24.3"

- name: internal-common # 私有 role 直接从 git 装
src: git+ssh://git@github.com/example/ansible-role-common.git
scm: git
version: v1.2.0

一条命令按清单安装:

ansible-galaxy role install -r requirements.yml
生产环境必须 pin 版本

requirements.yml 里不写版本等于永远装最新版,哪天上游发了个不兼容更新,你的部署就无声无息变了。生产环境把 version 钉死,升级时主动改版本号并过一遍测试环境。第三方 role 用之前也花五分钟读一遍它的 tasks,确认它到底对系统做了什么。

推荐项目布局​

官方 best practice 推荐的项目结构,环境(production / staging)各自独立成一套 inventory:

ansible-project/
├── ansible.cfg # 项目级配置:roles_path、默认 inventory 等
├── site.yml # 总入口 playbook,串联所有 role
├── requirements.yml # 外部 role 清单,ansible-galaxy install -r 用
├── inventories/
│ ├── production/
│ │ ├── hosts.ini # 生产主机清单
│ │ ├── group_vars/
│ │ │ ├── all.yml # 生产环境通用变量
│ │ │ └── web.yml # 生产 web 组的变量
│ │ └── host_vars/
│ │ └── web1.yml # 生产 web1 这台机器的特有变量
│ └── staging/
│ ├── hosts.ini # 测试环境主机清单
│ ├── group_vars/
│ │ ├── all.yml
│ │ └── web.yml
│ └── host_vars/
└── roles/
├── common/ # 自建 role:基础工具、时区、安全基线
└── nginx/ # 自建 role

逐个说:

  • inventories 按环境分目录:production 和 staging 完全隔离,执行时用 -i inventories/production 显式指定,从物理上杜绝「本想测 staging 结果打了生产」
  • group_vars 跟着 inventory 走:同一个 web 组,在生产环境是一套变量(比如 nginx_port: 80),在 staging 是另一套(比如 nginx_port: 8080),playbook 本身零改动
  • site.yml 是总入口:里面就是几个 play,按顺序引用 role
---
# site.yml
- name: 所有机器跑基础配置
hosts: all
become: true
roles:
- common

- name: web 组部署 nginx
hosts: web
become: true
roles:
- nginx

执行时指定环境:

ansible-playbook -i inventories/staging site.yml
ansible-playbook -i inventories/production site.yml
目录规模别一步到位

项目刚起步时,一个 inventory 文件、一个 group_vars、几个 role 就够了,没必要第一天就搭出完整骨架。等真的有了第二套环境再拆 inventories 目录,结构是随需求长出来的。

写好一个 role 的原则​

几条实操中总结的原则:

1. 单一职责

一个 role 只干一件事:装 nginx 的 role 不顺便配防火墙。粒度细了才好复用、好测试,组合的事交给 playbook。

2. 可配置项全部走 defaults

凡是「换个环境可能不一样」的值——端口、路径、版本号、开关——一律放 defaults/main.yml 并写上合理默认值。目标是不改 role 本体,只靠传变量适配所有环境。

3. 幂等,可重复跑

同一个 role 连续跑两遍,第二遍应该全是 ok 没有 changed。用模块(apt、template、service)而不是 shell 命令实现绝大部分操作;实在要用 shell,按上一篇说的补 creates: 或 changed_when。

4. 变量名加 role 前缀

所有变量以 role 名开头:nginx_port、mysql_root_password,不叫 port、password。Ansible 变量是全局命名空间,不加前缀迟早和别的 role 或 group_vars 撞名,而且撞了不报错、悄悄覆盖,极难排查。

5. 入口文件保持干净

tasks/main.yml 只做编排(判断发行版、import 子任务文件),具体实现拆到 tasks/install.yml、tasks/configure.yml。打开 main.yml 一眼能看到这个 role 干哪几件事。

小结​

  • role 是约定好的目录结构:tasks 必须有,其余目录按需;入口统一叫 main.yml
  • defaults 放「可覆盖的默认值」(优先级最低),vars 放「内部常量」(优先级高)
  • playbook 里 roles: 引用,role 默认从 playbook 同级 roles/ 目录发现;ansible-galaxy init 生成骨架
  • 外部 role 用 requirements.yml + ansible-galaxy install -r 管理,生产环境 pin 版本
  • 项目布局按环境拆 inventories,group_vars 跟着环境走,site.yml 做总入口
  • 写好 role 的口诀:单一职责、默认值走 defaults、幂等、变量名带前缀